feat(review): wire linked-issue satisfaction into the deterministic gate - #4069
Conversation
Wires the pure linked-issue satisfaction analysis core (src/services/linked-issue-satisfaction.ts, #2172) into a full model-calling orchestration + cache + gate pipeline: a new runGittensoryLinkedIssueSatisfaction service mirroring ai-slop.ts's multi-model retry/BYOK shape, a linked_issue_satisfaction_cache table keyed on (repo, pull, head SHA, linked issue number), and a new DB-backed gate.linkedIssueSatisfaction mode (off/advisory/block, default off) wired through the full aiReviewMode-style template (migration, schema, repositories resolver, OpenAPI, settings-preview, .gittensory.yml, docs). Extends fetchLinkedIssueFacts to also return the linked issue's title/body (same call, additive). Adds a linked_issue_scope_mismatch finding + isConfiguredGateBlocker branch: in block mode, an above-confidence-floor "unaddressed" verdict now fails the gate instead of only the AI reviewer's free-text prose flagging a scope mismatch while the structured Linked issue signal stays green. Closes #1961, Closes #3906
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | 073dfa8 | Commit Preview URL Branch Preview URL |
Jul 07 2026, 10:47 PM |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4069 +/- ##
==========================================
+ Coverage 93.60% 93.62% +0.01%
==========================================
Files 381 383 +2
Lines 35627 35738 +111
Branches 13070 13118 +48
==========================================
+ Hits 33347 33458 +111
Misses 1618 1618
Partials 662 662
🚀 New features to boost your workflow:
|
|
Warning 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏸️ Gittensory review result - manual review recommendedReview updated: 2026-07-07 23:01:48 UTC
⏸️ Suggested Action - Manual Review
Review summary Nits — 6 non-blocking
Concerns raised — review before merging
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
…on is unconsumed (#5308) True when written (#4069, 2026-07-07), invalidated three days later by #4149 (src/signals/focus-manifest.ts's resolveEffectiveSettings), which added a fallback-alias: when gate.linkedIssueSatisfaction is unset, review.linkedIssueSatisfaction IS folded in and feeds the real merge/close decision. None of the 7 subsequent commits touching this file corrected the comment. src/types.ts already carries the correct, current description. Comment-only, no behavior change. Refs #5284
… from the engine twin While mirroring contentLaneDeliverableGateMode into gate-advisory.ts, found that linkedIssueSatisfactionGateMode (#4069, merged 2026-07-07) was never mirrored either -- CI's engine-parity check only diffs against each PR's own changed files, so a prior PR that touched only the host twin without updating the engine twin (and without bumping packages/loopover-engine/package.json) went undetected. Any self-hoster consuming @loopover/engine's predicted-gate logic directly would silently never predict a linked_issue_scope_mismatch blocker for a repo with linkedIssueSatisfaction: block configured. Backfilled now while in the area.
* feat(review): add a config-as-code content-lane deliverable gate Registries with a "one entry file per contribution" content lane (e.g. an issue naming a specific path under a configured entryFileGlob) could have their linked issue auto-satisfied by a PR that never touches that path -- a test-only or unrelated-file PR closes the issue without delivering the content it asked for. Add checkContentLaneDeliverable(), a pure, spec-driven check: it scans the linked issue's text for a path token matching the resolved repo's RegistryLaneSpec and verifies the PR's changed files actually touch it. Wire it behind a new contentLaneDeliverableGateMode setting (off by default, advisory/block like every other gate dimension), fully wired through the DB, engine focus-manifest parser, OpenAPI schema, and .loopover.yml -- no repo-specific logic anywhere in the check itself, so any repo with a contentLane: config gets the same protection. * fix(engine): mirror the content-lane deliverable gate mode into the engine twin packages/loopover-engine/src/advisory/gate-advisory.ts is a hand-duplicated twin of src/rules/advisory.ts (#4518's gate-decision twin pair), enforced by scripts/check-engine-parity.ts's version-skew check. The new contentLaneDeliverableGateMode field and its isConfiguredGateBlocker branch were added to the host copy but never mirrored to the engine copy, so self-hosters consuming @loopover/engine directly would silently lack the new gate dimension, and CI's engine-parity check correctly failed the PR. * fix(engine): backfill the linked-issue satisfaction gate mode missing from the engine twin While mirroring contentLaneDeliverableGateMode into gate-advisory.ts, found that linkedIssueSatisfactionGateMode (#4069, merged 2026-07-07) was never mirrored either -- CI's engine-parity check only diffs against each PR's own changed files, so a prior PR that touched only the host twin without updating the engine twin (and without bumping packages/loopover-engine/package.json) went undetected. Any self-hoster consuming @loopover/engine's predicted-gate logic directly would silently never predict a linked_issue_scope_mismatch blocker for a repo with linkedIssueSatisfaction: block configured. Backfilled now while in the area.
Summary
src/services/linked-issue-satisfaction.ts) into a full model-calling orchestration + cache + gate pipeline -- the two pieces PR feat(review): add linked-issue satisfaction advisory + unified-comment section #3833 explicitly deferred ("the AI-call orchestration and any futuregate.linkedIssueSatisfactionmode wiring... is a separate, maintainer-only slice").src/services/linked-issue-satisfaction-run.tsmirrorsai-slop.ts'srunGittensoryAiSlopAdvisoryshape exactly: env/budget/BYOK gating, a bounded 2-model x 3-attempt retry/fallback loop, and confidence-floor-aware retry (an early below-floor "unaddressed" call retries; the floor is re-checked fresh on every independent attempt, never lowered).fetchLinkedIssueFacts(src/github/backfill.ts) to also return the linked issue'stitle/body-- same endpoint/call, purely additive optional fields; verified its 3 existing callers (linked-issue-hard-rules.ts,processors.ts,linked-issue-label-propagation-fetch.ts) are unaffected.gate.linkedIssueSatisfactionmode (off/advisory/block, defaultoff), wired through the fullaiReviewMode-style template: migration,schema.ts,types.ts,repositories.tsresolver (default/parse/insert/update),openapi/schemas.ts(both schemas),src/api/routes.tsmaintainer-settings write schema,settings-preview.ts,.gittensory.ymlgate.linkedIssueSatisfaction(packages/gittensory-engine'sFocusManifestGateConfig+ the app-sideapplyGateConfigOverridesresolver), and the 3 docs pages thecheck-docs-driftscript requires (docs.how-reviews-work.tsx,docs.tuning.tsx,docs.github-app.tsx).linked_issue_scope_mismatchfinding +isConfiguredGateBlockerbranch (src/rules/advisory.ts): closes fix(review): deterministic linked-issue gate never checks issue-PR scope match, only existence/openness #3906's gap. Inblockmode, an above-confidence-floor "unaddressed" verdict now actually fails the gate, instead of the structured "Linked issue" signal staying green while only the AI reviewer's free-text prose flagged the scope mismatch (the JSONbored/metagraphed PR fix(review): preserve gate verdict on auto-review skip #3910 repro).linked_issue_satisfaction_cachetable (mirrorsai_slop_cache, migration 0119): keyed on(repo, pull, head SHA, linked issue number). The extralinked_issue_numbercolumn (absent fromai_slop_cache) exists because a PR's primary linked issue can change between passes (an edited body re-links a different issue) -- reusing a stored verdict for a different issue would silently answer the wrong question.{status, rationale}throughunified-comment-bridge.tsinto the already-builtlinkedIssueSatisfactionBlock()renderer (feat(review): render linked-issue satisfaction as an advisory section in the unified comment #2174) -- no changes to the renderer itself, purely an additive passthrough field.Multi-linked-issue decision
v1 assesses only the PR's primary (first) linked issue, not every linked issue a PR might cite. Documented in
runLinkedIssueSatisfactionForAdvisory's doc comment (src/queue/processors.ts) and the cache migration's comment. Rationale:linkedIssueNumbercache-key column, not an array), keeping the schema and orchestration simple.A future slice could widen this to assess every linked issue independently if multi-issue PRs turn out to be common enough to matter; documenting the choice here rather than building it speculatively.
Design note:
gate.linkedIssueSatisfactionvs. the pre-existingreview.linkedIssueSatisfactionPR #3665 already parses a
review.linkedIssueSatisfactionconfig-as-code field (#2173) -- but it's YAML-only (no DB column), lives under the content-orientedreview:manifest block (alongsidefooter.text,fields:{...}show/hide toggles), and per its own PR description is unconsumed by any decision path yet. This PR adds a new, distinctgate.linkedIssueSatisfactionfield (mirroringaiReviewMode's exact DB + dashboard + yml wiring) rather than repurposing that one:gate.linkedIssueSatisfactionmode wiring" as the intended name for this slice.gate.*+ a DB column (aiReviewMode,selfAuthoredLinkedIssueGateMode,manifestPolicyGateMode, ...);review.*is reserved for presentation-only toggles. Mixing a blocking decision into the content-config block would be a real architectural inconsistency, not just a naming quibble.aiReviewMode-style template, not the YAML-only pattern.review.linkedIssueSatisfactionis left untouched and unconsumed by this PR -- flagging it here for a maintainer to decide whether to deprecate it in a follow-up, since having two same-named-leaf config surfaces (review.linkedIssueSatisfactionvsgate.linkedIssueSatisfaction) is a real footgun if a maintainer sets the wrong one.Closes #1961, Closes #3906.
Scope
type(scope): short summaryConventional Commit format.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run actionlintnpm run typechecknpm run test:coveragelocally -- every changed/addedsrc/**line and branch in this diff is covered (verified by cross-referencingcoverage/lcov.infoagainst the exact diff hunks)npm run test:workersnpm run build:mcpnpm run test:mcp-packnpm run ui:openapi:checknpm run ui:lintnpm run ui:typechecknpm run ui:buildnpm audit --audit-level=moderateSafety
UI Evidencesection. Not applicable: the onlyapps/gittensory-ui/**changes are additional<li>bullet text in 3 existing docs pages (docs.how-reviews-work.tsx,docs.tuning.tsx,docs.github-app.tsx) -- same list structure, no new component/layout/visual state to screenshot.CHANGELOG.mditself is untouched.Notes
packages/gittensory-enginerequiresnpm run build --workspace=@jsonbored/gittensory-enginebeforenpm run test:coveragewill resolve on a genuinely fresh checkout (no priordist/) -- pre-existing gap intest:ci's own script ordering (test:coverageruns beforebuild:miner, which is what builds the engine package), unrelated to this PR's code. Filed separately for a maintainer follow-up rather than folded into this PR.